﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using CFScript.Tasks;
using System.Text.RegularExpressions;
using System.Collections.ObjectModel;
using System.IO;
using CFScript.PInvoke;

namespace CFScript
{
    public enum ResultCode
    {
        OK = 0,
        ERROR = -1,
        WARN = 1
    }

    /// <summary>
    /// This class contains information about the execution result of a Task or Instruction of CFScript. The main property is the execution Code, but also contains information
    /// about the executed instruction, an information message and a flag that indicates if the execution of the script must be broken after execution
    /// </summary>
    public class InstructionResult
    {
        ResultCode code = ResultCode.OK;
        Instruction executedInstruction;
        bool breakExecution = false;
        string resultMessage;

        public ResultCode Code
        {
            get { return this.code; }
            set { this.code = value; }
        }
        public bool BreakExecution
        {
            get { return this.breakExecution; }
            set { this.breakExecution = value; }
        }

        public Instruction ExecutedInstruction
        {
            get { return this.executedInstruction; }
            set { this.executedInstruction = value; }
        }

        public string Message
        {
            get { return this.resultMessage; }
            set { this.resultMessage = value; }
        }
    }

    /// <summary>
    /// Main CFScript class. It's responsible of load and execute scripts.
    /// </summary>
    public class CFScript:RunnableItem
    {
        struct RuntimeVariableStruct
        {
            private readonly string name;
            private readonly bool isDynamic;

            public RuntimeVariableStruct(string name, bool isDynamic)
            {
                this.name = name;
                this.isDynamic = isDynamic;
            }

            public string Name { get { return name; } }
            public bool IsDynamic { get { return isDynamic; } }
        };

        public Options Options
        {
            get { return base.options; }
        }

        static readonly IList<RuntimeVariableStruct> runtimeVariables = new ReadOnlyCollection<RuntimeVariableStruct>
            (new[] {
                new RuntimeVariableStruct(@"CFS_PATH", false), // Full path of the script that is being executed (if loaded from a file)
            new RuntimeVariableStruct(@"CFS_FILE", false),
            new RuntimeVariableStruct(@"CFS_ORIGIN", false),
            new RuntimeVariableStruct(@"AppName", false),
            new RuntimeVariableStruct(@"AppPath", false),
            new RuntimeVariableStruct(@"ProgramFiles", false),
            new RuntimeVariableStruct(@"DeviceName", false),
            new RuntimeVariableStruct(@"DeviceSerial", false),
            new RuntimeVariableStruct(@"DeviceIMEI", false),
            new RuntimeVariableStruct(@"DeviceOSName", false),
            new RuntimeVariableStruct(@"DeviceOSVersion", false),
            new RuntimeVariableStruct(@"DeviceOSCommonName", false),
            new RuntimeVariableStruct(@"DateTimeNow", true),
            new RuntimeVariableStruct(@"DateNow", true),
            new RuntimeVariableStruct(@"TimeNow", true),
            new RuntimeVariableStruct(@"HKLM", false)
            });

        
        string author;
        string timestamp;
        string comments;

        bool optionsSet = false;

        protected Dictionary<string, Variable> variables;
        protected List<Instruction> instructions;
        protected TextWriter output;

        public string Author
        {
            get { return this.author; }
        }

        public Dictionary<string, Variable> Variables
        {
            get { return this.variables; }
        }

        public TextWriter OutputStream
        {
            get { return this.output; }
        }

        #region Constructors

        public CFScript()
        {
            this.options = new Options();
            this.variables = new Dictionary<string, Variable>();
            this.instructions = new List<Instruction>();
        }

        public CFScript(string scriptPath) : this()
        {
            XmlDocument doc = new XmlDocument();
            doc.Load(scriptPath);
            LoadScriptFromXml(doc);
        }

        public CFScript(XmlDocument scriptDocument) : this()
        {
            LoadScriptFromXml(scriptDocument);
        }

        #endregion

        #region Script loaders

        private void LoadScriptFromXml(XmlDocument scriptDocument)
        {
            XmlElement root = scriptDocument.DocumentElement;

            if (root.Name != "cfs")
            {
                throw new ScriptParseException("Invalid File. Root element must be <cfs>");
            }
            else
            {
                if (root.HasAttribute("author"))
                {
                    this.author = root.GetAttribute("author");
                }

                if (root.HasAttribute("timestamp"))
                {
                    this.timestamp = root.GetAttribute("timestamp");
                }
                
                if (root.HasAttribute("comments"))
                {
                    this.comments = root.GetAttribute("comments");
                }

                foreach (XmlElement node in root.ChildNodes)
                {
                    if (node.Name == "options")
                    {
                        if (!this.optionsSet)
                        {
                            foreach (XmlElement optionsNode in node.ChildNodes)
                            {
                                if (optionsNode.Name == "onError")
                                {
                                    if(optionsNode.Attributes["action"] != null)
                                    {
                                        string action = optionsNode.Attributes["action"].Value;
                                        this.options.OnError = (OnErrorAction)Enum.Parse(typeof(OnErrorAction), action, true);
                                    }
                                }
                                else if (optionsNode.Name == "set")
                                {
                                    this.instructions.Add(Instruction.InstructionFactory(optionsNode));
                                    
                                }
                                else if (optionsNode.Name == "verboseMode")
                                {
                                    if (optionsNode.Attributes["level"] != null)
                                    {
                                        string action = optionsNode.Attributes["level"].Value;
                                        this.options.VerboseLevel = (VerboseMode)Enum.Parse(typeof(VerboseMode), action, true);
                                    }
                                }
                                else
                                {
                                    throw new ScriptParseException(string.Format("Option {0} not supported.", optionsNode.Name));
                                }
                            }
                            this.optionsSet = true;
                        }
                    }
                    else
                    {
                        this.instructions.Add(Instruction.InstructionFactory(node));
                    }
                }
            }
        }

        #endregion

        #region Execution control

        public ScriptResult Execute()
        {
            return Execute((List<Variable>)null);
        }

        public ScriptResult Execute(List<Variable> variables, TextWriter output)
        {
            this.output = output;
            return Execute(variables);
        }

        public ScriptResult Execute(TextWriter output)
        {
            this.output = output;
            return Execute((List<Variable>)null);
        }

        public ScriptResult Execute(List<Variable> initialVariables)
        {
            ScriptResult result = new ScriptResult();

            try
            {
                // Load runtime variables
                foreach (RuntimeVariableStruct rtmVarPrototype in runtimeVariables)
                {
                    Variable rtmVariable = new Variable(rtmVarPrototype.Name);
                    rtmVariable.IsRuntimeVariable = true;
                    rtmVariable.IsDynamicRuntimeVariable = rtmVarPrototype.IsDynamic;
                    AddVariable(rtmVariable);
                }

                // Load initial variables
                if (initialVariables != null)
                {
                    foreach (Variable vari in initialVariables)
                    {
                        AddVariable(vari);
                    }
                }

                // Execute the script
                foreach (Instruction i in this.instructions)
                {
                    // TODO: instructions can override OnErrorAction
                    InstructionResult iResult = i.Execute(this);
                    result.InstructionResults.Add(iResult);
                    if (iResult.BreakExecution || (iResult.Code == ResultCode.ERROR && this.options.OnError == OnErrorAction.BREAK))
                    {
                        break;
                    }

                }
            }
            catch (Exception ex)
            {
                result.Code = ResultCode.ERROR;
                result.Message = ex.Message;
            }

            return result;
        }

        #endregion

        #region Context methods

        public bool AddVariable(Variable variable)
        {
            bool result = this.variables.ContainsKey(variable.Name.ToLower());

            this.variables[string.Format("${{{0}}}", variable.Name).ToLower()] = variable;
            
            return result;
        }

        public string ExpandValue(string value)
        {
            Regex RE = new Regex(@"\$\{[A-Za-z]([A-Za-z]|[0-9])*\}", RegexOptions.IgnoreCase);
            string result = value;

            try
            {
                result = RE.Replace(value, m =>
                {
                    if (this.variables.ContainsKey(m.Value.ToLower()))
                    {
                        if (this.variables[m.Value.ToLower()].IsRuntimeVariable)
                        {
                            return GetRuntimeVariableValue(this.variables[m.Value.ToLower()]);
                        }
                        else
                        {
                            return this.variables[m.Value.ToLower()].Value;
                        }
                    }
                    else
                        return m.Value;
                });
            }
            catch (Exception ex)
            {

            }

            return result;
        }

        // TODO: implement properly. Right now it's just a dummy implementation
        private string GetRuntimeVariableValue(Variable variable)
        {
            string result = "";

            if (string.IsNullOrEmpty(variable.Value) || variable.IsDynamicRuntimeVariable)
            {
                switch (variable.Name.ToLower())
                {
                    case "cfs_path":
                        result = "cfs_path";
                        break;
                    case "cfs_file":
                        result = "cfs_file";
                        break;
                    case "cfs_origin":
                        result = "cfs_origin";
                        break;
                    case "appname":
                        //result = System.IO.Path.GetFileName(System.Reflection.Assembly.GetCallingAssembly().GetName().CodeBase);
                        result = System.IO.Path.GetFileName(Runtime.GetCallingExe());
                        break;
                    case "apppath":
                        result = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetCallingAssembly().GetName().CodeBase);
                        break;
                    case "programfiles":
                        result = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
                        break;
                    case "devicename":
                        result = "devicename";
                        break;
                    case "deviceserial":
                        result = PInvoke.Device.GetDeviceID();
                        break;
                    case "deviceimei":
                        result = PInvoke.Device.GetDeviceIMEI();
                        break;
                    case "deviceosname":
                        result = "N/A";
                        try
                        {
                            Device dev = Platform.GetDevice();
                            result = dev.os.name;
                        }
                        catch
                        {
                        }
                        break;
                    case "deviceosversion":
                        result = "N/A";
                        try
                        {
                            Device dev = Platform.GetDevice();
                            result = dev.os.wm_version;
                        }
                        catch
                        {
                        }
                        break;
                    case "deviceoscommonname":
                        result = "N/A";
                        try
                        {
                            Device dev = Platform.GetDevice();
                            result = dev.os.fullName;
                        }
                        catch
                        {
                        }
                        break;
                    case "datetimenow":
                        result = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss");
                        break;
                    case "datenow":
                        result = DateTime.Now.ToString("yyyy/MM/dd");
                        break;
                    case "timenow":
                        result = DateTime.Now.ToString("HH:mm:ss");
                        break;
                    case "hklm":
                        result = "HKEY_LOCAL_MACHINE";
                        break;
                }
                variable.Value = result;
                AddVariable(variable);
            }
            else
            {
                result = variable.Value;
            }

            return result;
        }

        #endregion
    }
}
